package swingExtended.customTextPane;

import function.unary.UnaryFunction;
import function.unary.UnaryPredicate;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
import javax.swing.text.AttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import list.UnorderedArrayList;
import list.functions.ListFunctions;

/**
 * A TextPane that when given a String can attempt to find a font that will
 * display the String.
 * 
 * @author g1wshimu
 */
public class DynamicFontTextPane extends SimpleTextPane{
    
    // <editor-fold defaultstate="collapsed" desc="Private Instance Variables">
    /** 
     * A mapping between the code point of a character and the font that displays it.
     */
    private final TreeMap<Integer, Font> FONT_MAPPING = new TreeMap<Integer, Font>();

    /** A list of all the fonts available for use by the program that were
     * not used at some point to display a character that was supported by
     * the default or given font. Disjoint from GOOD_FONTS. */
    private final UnorderedArrayList<Font> ALL_FONTS =
            new UnorderedArrayList<Font>();

    /** A list of all the fonts that were used at some point to display a 
     * character that was unsupported by the default or given font. Disjoint
     * from ALL_FONTS. */
    private final ArrayList<Font> GOOD_FONTS = new ArrayList<Font>();
    // </editor-fold>
    
    public DynamicFontTextPane() {

        // Grab all the fonts available 
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();

        // Retrieve a list of fonts available in this GraphicsEnvironment
        List<String> fontNames =
                Arrays.asList(ge.getAvailableFontFamilyNames());

        // Convert the list of strings into a list of fonts
        List<Font> fonts = ListFunctions.map(
                new UnaryFunction<String, Font>() {

                    @Override
                    public Font evaluate(String arg) {
                        return Font.getFont(arg);
                        //new Font(arg, DEFAULT_STYLE, DEFAULT_SIZE);
                    }
                }, fontNames);

        ListFunctions.addAll(fonts, ALL_FONTS);
    }
    
    /**
     * Prints the given msg to the screen using the given preferredFontName and color.
     * @param msg the message to be printed to the screen
     * @param preferredFontName the preferredFontName of the message
     * @param c the color of the message
     * @return true if the writing was successful, false otherwise.
     */    
    public void print(String msg, String preferredFontName) {
        final StyleContext STYLE_CONTEXT = StyleContext.getDefaultStyleContext();
        AttributeSet aset = STYLE_CONTEXT.addAttribute(
                SimpleAttributeSet.EMPTY, StyleConstants.Foreground, this.getColor());
        aset = STYLE_CONTEXT.addAttribute(aset, StyleConstants.Alignment,
                StyleConstants.ALIGN_JUSTIFIED);

        if (willFindCompatibleFont()) {
            Font preferredFont = new Font(preferredFontName,
                    DEFAULT_STYLE, DEFAULT_SIZE);

            for (int i = 0; i < msg.length(); i++) {
                final int codePoint = msg.codePointAt(i);

                final Font compatibleFont;

                if (preferredFont.canDisplay(codePoint)) {
                    compatibleFont = preferredFont;
                } else {
                    compatibleFont = getCompatibleFont(codePoint);
                }

                final String displayMsg;
                if (compatibleFont == null) {
                    displayMsg = REPLACEMENT_CHAR;
                    aset = STYLE_CONTEXT.addAttribute(aset, StyleConstants.FontFamily,
                            preferredFontName);
                } else {
                    displayMsg = new String(new int[]{codePoint}, 0, 1);
                    aset = STYLE_CONTEXT.addAttribute(aset, StyleConstants.FontFamily,
                            compatibleFont.getName());
                }

                output(displayMsg, aset);
            }
        } else {
            aset = STYLE_CONTEXT.addAttribute(aset, StyleConstants.FontFamily,
                    preferredFontName);
            output(msg, aset);
        }

        this.validate();
    }
    
    // <editor-fold defaultstate="collapsed" desc="Helper Methods">
    private Font findCompatibleFont(final int codePoint) {
        // Look at fonts that were used before as a compatible font
        Font compatibleFont = ListFunctions.find(new UnaryPredicate<Font>() {

            @Override
            public Boolean evaluate(Font font) {
                return font.canDisplay(codePoint);
            }
        }, GOOD_FONTS);

        if (compatibleFont != null) {
            return compatibleFont;
        }

        // Look at the rest of the fonts if a compatible one was not
        // found
        for (int i = 0; i < ALL_FONTS.size(); i++) {
            Font font = ALL_FONTS.get(i);

            if (font.canDisplay(codePoint)) {
                //System.err.printf("Found font: %s for character (code point): %s\n",
                //        compatibleFont, codePoint);
                GOOD_FONTS.add(font);
                ALL_FONTS.remove(i);
                return font;
            }
        }
        return null;
    }

    private Font getCompatibleFont(int codePoint) {
        // First look to see if we've encountered this codePoint before
        if (FONT_MAPPING.containsKey(codePoint)) {
            return FONT_MAPPING.get(codePoint);
        }
        Font font = findCompatibleFont(codePoint);
        FONT_MAPPING.put(codePoint, font);
        return font;
    }
    // </editor-fold>
    
    

    /**
     * Return a list of code points (in hexidecimal String) that could not be 
     * displayed by any of the fonts available on this system. 
     * 
     * @return 
     */
    public List<String> getUnsupportedCodePoints() {
        List<Integer> unsupportedChars = ListFunctions.filter(new UnaryPredicate<Integer>() {

            @Override
            public Boolean evaluate(Integer arg) {
                return FONT_MAPPING.get(arg) == null;
            }
        }, FONT_MAPPING.keySet());

        return ListFunctions.map(new UnaryFunction<Integer, String>() {

            @Override
            public String evaluate(Integer arg) {
                return Integer.toHexString(arg);
            }
        }, unsupportedChars);
    }
}
